Skip to content

feat(session_waiter): allow session_waiter handler to return MessageEventResult - #9551

Open
Hiraeth-Wave wants to merge 4 commits into
AstrBotDevs:masterfrom
Hiraeth-Wave:feat/session-wainer-return-result
Open

feat(session_waiter): allow session_waiter handler to return MessageEventResult#9551
Hiraeth-Wave wants to merge 4 commits into
AstrBotDevs:masterfrom
Hiraeth-Wave:feat/session-wainer-return-result

Conversation

@Hiraeth-Wave

@Hiraeth-Wave Hiraeth-Wave commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Resolve #9548.

Modifications / 改动点

  • 这不是一个破坏性变更。

核心改动(3 文件)

  • astrbot/core/utils/session_waiter.pySessionWaiter.trigger 返回 handler 的返回值(原为丢弃),返回类型 NoneAny
  • astrbot/builtin_stars/astrbot/main.pyhandle_session_control_agent 改为 async generator——当 trigger 返回 MessageEventResultyield 它,借助 scheduler 的洋葱模型自动执行 ResultDecorateStageRespondStage(发送);否则保持原 stop_event 行为。
  • astrbot/core/platform/astr_message_event.py:修复 stop_event() / continue_event()_result is None 时凭空创建空 MessageEventResult(chain=[]) 的行为,改为只设置 _force_stopped 标志。

Bug 修复说明(stop_event 空发送)

在改 handle_session_control_agent 为 async generator 后,实测发现每次 waiter 返回 MessageEventResultRespondStage 会触发两次:一次发送实际内容,一次发送空内容(日志 Prepare to send - (空))。

根因是三个机制叠加:

  1. yield resultRespondStage 发送实际内容后调 clear_result()event._result = None
  2. 随后 event.stop_event() 发现 _result is None凭空造一个空 MessageEventResult(chain=[]) 挂回去(旧代码第 347 行 self.set_result(MessageEventResult().stop_event()))。
  3. scheduler 的洋葱模型在 ProcessStage 结束后继续遍历后续 stage,RespondStage 看到非 None 的空 result,再次进入发送分支(chain 为空不会真正 send,但会打印日志并触发 OnAfterMessageSentEvent 钩子)。

is_stopped() 第一行就检查 _force_stopped,根本不需要那个空 result;ResultDecorateStage / RespondStage 入口都有 if result is None: return 保护。因此移除空 result 创建是安全的,且彻底消除了空发送。

测试(3 文件,9 个用例)

  • tests/unit/test_session_waiter.py:覆盖 trigger 在 handler 返回 MessageEventResult / None / 抛异常 / 无对应 session 四种情况下的返回值。
  • tests/unit/test_session_control_agent.py:覆盖 handle_session_control_agent 在 trigger 返回 MessageEventResult(yield + stop)/ None(无 yield + stop)/ 无匹配 session(无操作)三种分支。
  • tests/unit/test_astr_message_event.py:更新原有的 test_stop_event_creates_result_if_none / test_continue_event_creates_result_if_none 两个用例,断言新行为(不再创建空 result,_result 保持 Noneis_stopped()_force_stopped 返回正确值)。

工作机制call_handler 检测到 handler 是 async generator 且 yieldMessageEventResult 时,会自动 event.set_result(ret) 再向上 yield,触发 scheduler 递归执行后续 stage。因此 waiter handler 只需 return event.plain_result(...),回复即走完整装饰流程。

向后兼容

  • 老插件在 waiter 内用 event.send() 且不 return 的,handler 返回 None,走原 stop_event 路径,行为完全不变。
  • stop_event() 的语义不变(is_stopped() 仍正确返回 True),只是不再产生空 result 副作用。经全局排查,所有 stop_event() 调用点要么在 stage 内部(stop 后 pipeline break),要么有 if result is None: return 保护,无代码依赖「stop 后 get_result() 一定非 None」。

文档更新:更新会话控制文档,使用新调用方法,增加相关提示。此外顺带更新了过时的 import

插件侧用法示例

改前(装饰功能失效):

@session_waiter(timeout=60)
async def idiom_waiter(controller: SessionController, event: AstrMessageEvent):
    ...
    await event.send(event.plain_result("先见之明"))  # 不走 @回复 / 引用 / 分段
    controller.keep(timeout=60, reset_timeout=True)

改后(装饰功能生效):

@session_waiter(timeout=60)
async def idiom_waiter(controller: SessionController, event: AstrMessageEvent):
    ...
    controller.keep(timeout=60, reset_timeout=True)
    return event.plain_result("先见之明")  # 自动走 @回复 / 引用 / 分段 / TTS / 转图

Screenshots or Test Results / 运行截图或测试结果

$ uv run pytest tests/unit/test_session_control_agent.py tests/unit/test_session_waiter.py tests/unit/test_astr_message_event.py -q
85 passed, 1 warning in 13.11s

warning 为已有的弃用依赖警告(audioop),与本次 PR 无关。

用于测试此改动的插件:Hiraeth-Wave/astrbot_plugin_test/tree/test/astrbot-feat-9551,可下载源码压缩包后自行安装测试。


Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
  • 👀 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 requirements.txtpyproject.toml 文件相应位置。
  • 😮 我的更改没有引入恶意代码。

Let session_waiter handlers return a MessageEventResult instead of forcing them to call event.send(). The returned result is yielded back into the pipeline by handle_session_control_agent, so it goes through the full ResultDecorateStage (reply-with-mention, reply-with-quote, segmented reply, TTS, text-to-image, etc.) and RespondStage like any normal reply.

Backward compatible: handlers that return None keep the original stop_event behavior.
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. area:core The bug / feature is about astrbot's core, backend labels Aug 4, 2026
@dosubot

dosubot Bot commented Aug 4, 2026

Copy link
Copy Markdown

📄 Knowledge review

Dosu skipped reviewing this PR because your organization has used its 200 included credits for the month. Your usage will reset on 2026-09-01. To have Dosu review this PR before then, ask your organization admin to upgrade to a pro account.


Leave Feedback Ask Dosu about AstrBot Add Dosu to your team

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

When stop_event() was called after RespondStage had already sent and cleared the result (e.g. in handle_session_control_agent after yielding a MessageEventResult), it fabricated an empty MessageEventResult(chain=[]) and re-attached it to the event. The scheduler's onion model then ran RespondStage again on this empty result, producing a spurious second 'Prepare to send - (empty)' log (and triggering after_message_sent hooks a second time).

Now stop_event()/continue_event() only set _force_stopped; they no longer create a result when none exists. is_stopped() already checks _force_stopped first, so stop semantics are unchanged. All stage entry points (ResultDecorateStage, RespondStage) guard with 'if result is None: return', so the empty second invocation is eliminated.

Updated the two existing unit tests that asserted the old fabricating behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core The bug / feature is about astrbot's core, backend size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] session_waiter 支持返回 MessageEventResult 以走完整消息装饰流程

1 participant